SPB Git

Repository Public

page.tsx 3.7 KB 82 lines tsx

1import type { Metadata } from "next";2import { Suspense } from "react";3import { NodesExplorer } from "@/components/nodes/NodesExplorer";4import { PageIntro, SectionHead } from "@/components/ui/Section";5import { nodes, planned, timeline } from "@/lib/data";6import { formatDate, formatNumber, getDict, type Locale } from "@/lib/i18n";7import { aggregate } from "@/lib/inventory";8import { pageMetadata } from "@/lib/seo";910type Props = { params: Promise<{ locale: Locale }> };1112export async function generateMetadata({ params }: Props): Promise<Metadata> {13  return pageMetadata((await params).locale, "nodes", "/nodes");14}1516export default async function NodesPage({ params }: Props) {17  const { locale } = await params;18  const t = getDict(locale);19  const n = t.nodes;20  const agg = aggregate(nodes);21  const stats = [22    { v: formatNumber(agg.documented, locale), l: n.stats.machines },23    { v: agg.unifiedMemoryGB !== null ? `${formatNumber(agg.unifiedMemoryGB, locale)} ${t.common.gb}` : t.common.notDocumented, l: n.stats.memory },24    { v: agg.cpuCores !== null ? formatNumber(agg.cpuCores, locale) : t.common.notDocumented, l: n.stats.cpu },25    { v: agg.lastVerifiedAt ? formatDate(agg.lastVerifiedAt, locale) : t.common.notDocumented, l: n.stats.verified },26  ];27  return (28    <>29      <PageIntro eyebrow={n.eyebrow} title={n.title} intro={n.intro} />30      <section className="shell">31        <ul className="grid grid-cols-2 gap-3 lg:grid-cols-4" data-testid="inventory-stats">32          {stats.map((s) => (33            <li key={s.l} className="card p-4 md:p-5">34              <p>35                <span className="num block text-xl font-semibold tracking-[-0.03em] md:text-3xl">{s.v}</span>{" "}36                <span className="label mt-2 block">{s.l}</span>37              </p>38            </li>39          ))}40        </ul>41        <div className="mt-6">42          <Suspense fallback={<p className="card p-10 text-center text-mute">…</p>}>43            <NodesExplorer nodes={nodes} locale={locale} />44          </Suspense>45        </div>46      </section>4748      <section className="shell mt-24 md:mt-36">49        <SectionHead eyebrow={n.timeline.eyebrow} title={n.timeline.title} intro={n.timeline.intro} />50        <ol className="mt-12 border-l border-line-strong md:ml-[25%]">51          {timeline.map((e) => (52            <li key={e.date + e.title.en} className="reveal relative pb-10 pl-7 last:pb-0">53              <span aria-hidden="true" className="absolute -left-[4.5px] top-1.5 h-2 w-2 rounded-full border-2 border-accent bg-bg" />54              <time dateTime={e.date} className="num font-mono text-[0.75rem] tracking-[0.06em] text-accent">{formatDate(e.date, locale)}</time>55              <h3 className="mt-2 text-xl font-medium tracking-[-0.02em]">{e.title[locale]}</h3>56              <p className="mt-2 max-w-[66ch] text-[0.9375rem] leading-relaxed text-body">{e.detail[locale]}</p>57            </li>58          ))}59        </ol>60      </section>6162      <section className="shell mt-24 md:mt-36">63        <SectionHead eyebrow={n.planned.eyebrow} title={n.planned.title} />64        <div className="mt-10 md:ml-[25%]">65          {planned.length === 0 ? (66            <p className="card max-w-[70ch] border-dashed p-6 text-[0.9375rem] leading-relaxed text-body">{n.planned.empty}</p>67          ) : (68            <ul className="divide-y divide-line border-y border-line">69              {planned.map((p) => (70                <li key={p.label.en} className="py-5">71                  <p className="font-medium">{p.label[locale]} <span className="label ml-3 !text-accent">{t.common.lifecycle.planned}</span></p>72                  <p className="mt-1 text-[0.9375rem] text-body">{p.note[locale]}</p>73                </li>74              ))}75            </ul>76          )}77        </div>78      </section>79    </>80  );81}82
Files main
Menu

Theme